function MVVM(options) {
  this.$options = options;
  // 原数据
  var data = (this._data = this.$options.data);
  var me = this;

  // 1. 数据代理：将data原数据中属性代理到this上，可以通过this直接访问
  // 遍历data原数据所有属性，对每一个属性进行数据代理
  // 数据代理通过Object.defineProperty将属性定义在this上，同时定义属性get/set...
  // 当你通过this读取属性时，实际读取是data原数据属性的值
  // 当你通过this设置属性时，实际设置是data原数据属性的值
  Object.keys(data).forEach(function (key) {
    me._proxy(key);
  });

  // 2. 数据劫持：将data原数据中所有属性进行重新定义，定义成响应式
  // 提取data中所有属性进行遍历, 所有属性一个一个进行Object.defineProperty重新定义
  // 定义成响应式。
  // 每一个响应式属性都有一个自己的dep对象，通过闭包的方式保存再get/set方法中
  observe(data, this);

  // 3. 模板解析：解析模板页面中插值语法和指令语法
  /*
    1. 将el的所有子元素添加到文档碎片节点
      创建文档碎片节点
      遍历el所有子元素，添加到文档碎片节点中
      返回文档碎片节点
    2. 解析文档碎片节点中模板语法 
      遍历所有子节点，判断子节点是元素节点还是文本节点
        如果是元素节点，要解析里面的指令语法
          提取节点所有属性节点，遍历，判断是否是指令语法(v-)
          再判断是事件指令(on)还是普通指令
            事件指令：给当前节点绑定事件和相应的回调函数，回调函数通过bind方法改变this指向为vm
            普通指令：调用相应的更新DOM元素的updater方法更新DOM的值
              比如：v-text, 调用textUpdater，更新元素textContent属性
              比如：v-html, 调用htmlUpdater，更新元素innerHTML属性
              最后，new Watcher
          删除解析后的指令属性
        如果是文本节点，要解析里面的插值语法
          解析方式最终和v-text是一致的，调用textUpdater，更新元素textContent属性
      递归编译当前子节点所有子节点，为了让所有节点都要被解析
    3. 将解析好文档碎片节点添加到el，生效
  */
  this.$compile = new Compile(options.el || document.body, this);
}

MVVM.prototype = {
  $watch: function (key, cb, options) {
    new Watcher(this, key, cb);
  },

  // 数据代理方法
  _proxy: function (key) {
    var me = this;
    Object.defineProperty(me, key, {
      configurable: false,
      enumerable: true,
      get: function proxyGetter() {
        return me._data[key];
      },
      set: function proxySetter(newVal) {
        // 触发数据劫持设置的set方法
        me._data[key] = newVal;
      },
    });
  },
};
